/**
 * <AiRmob-Framework; A simple augmented reality framework for android >
    Copyright (C) <2012>  <AiRmob Team>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package rendercomponents;

import java.util.LinkedList;
import java.util.Stack;

import android.opengl.Matrix;

public class SceneManager implements SceneManagerInterface {

	private Stack<float[]> transformationStack;
	private Renderer renderer;

	// LinkedList that stores the results from the tree traversal of the scene
	// graph - after sorting contains only models while lights are stored in
	// allLights
	private LinkedList<Identifier> allObjects;

	// LinkedList that stores all lights
	private LinkedList<Identifier> allLights;

	public SceneManager(Renderer r) {
		renderer = r;
		this.transformationStack = new Stack<float[]>();
		this.setAllObjects(new LinkedList<Identifier>());
	}

	/**
	 * Builds (and sorts) the scene graph.
	 */
	public void buildSceneGraph() {
		this.setAllObjects(new LinkedList<Identifier>());
		this.setAllLights(new LinkedList<Identifier>());

		this.build(renderer.scene);

		sortSceneGraph();
	}

	/**
	 * Only sorts the scene graph.
	 */
	public void sortSceneGraph() {
		LinkedList<Identifier> swap = new LinkedList<Identifier>();

		// Put all objects in the following order into the list:
		// lights in allLights
		// models and texts in the following order:
		// opaque objects, transparent-objects from back to front and then texts
		// texts last to avoid unnecessary state-changes
		for (Identifier p : getAllObjects()) {
			if (p.holdsLight) {
				getAllLights().add(p);
			}
		}

		for (Identifier p : getAllObjects()) {
			if (p.holdsModel3D || p.holdsModelGroup || p.holdsModelAnimation) {
				if (!p.holdsTransparentChild)
					swap.add(p);
			}
		}

		int size = swap.size();
		boolean first = true;

		for (Identifier p : getAllObjects()) {
			if (p.holdsModel3D || p.holdsModelGroup || p.holdsModelAnimation) {
				if (p.holdsTransparentChild) {
					if (renderer.settings.sortObjectsByDistance) {
						if (first) {
							swap.add(p);
							p.distance = calculateDistance(p);
							first = false;
						} else {

							p.distance = calculateDistance(p);

							swap.add(p);

							for (int j = swap.size() - 1; j > size; j--) {
								if (p.distance > swap.get(j - 1).distance) {
									swap.set(j, swap.get(j - 1));
									swap.set(j - 1, p);
								} else {
									break;
								}
							}
						}

					} else {
						swap.add(p);
					}
				}
			}
		}

		for (Identifier p : getAllObjects()) {
			if (p.holdsText) {
				swap.add(p);
			}
		}

		this.setAllObjects(swap);

	}

	// ////////////////////////////////////////////////////////
	// /Calculates the distance from given identifier to camera
	// ////////////////////////////////////////////////////////

	private float calculateDistance(Identifier id) {
		float[] p1 = new float[4];

		float[] mModelMatrix = id.mTotalTransformationMatrix;
		float[] mViewMatrix = renderer.mViewMatrix;

		float[] mModelViewMatrix = new float[16];

		Matrix.multiplyMM(mModelViewMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);

		if (id.holdsModel3D) {
			id.getModelChild().mesh.bVertexBuffer.position(0);
			id.getModelChild().mesh.bVertexBuffer.get(p1, 0, 3);
		} else if (id.holdsModelAnimation) {
			id.animationChild.getFrames().get(renderer.frameCount).mesh.bVertexBuffer
					.get(p1, 0, 3);
		} else if (id.holdsModelGroup) {
			id.modelGroupChild.models.get(0).mesh.bVertexBuffer.get(p1, 0, 3);
		}

		p1[3] = 1;

		Matrix.multiplyMV(p1, 0, mModelViewMatrix, 0, p1, 0);

		float distance = (float) Math
				.sqrt((p1[0] * p1[0] + p1[1] * p1[1] + p1[2] * p1[2]));

		return distance;
	}

	// ////////////////////////////////////////////////////////
	// /Draws all model3Ds in the scene graph
	// ////////////////////////////////////////////////////////

	private void build(Parent object) {
		if (object.type == 0) {
			transformationStack = new Stack<float[]>();
			transformationStack.push(object.mTransformationMatrix);
		} else {

			float[] stackElement = new float[16];
			Matrix.multiplyMM(stackElement, 0, object.mTransformationMatrix, 0,
					transformationStack.peek(), 0);
			transformationStack.push(stackElement);
		}

		if (object.type == 3 || object.type == 9 || object.type == 7
				|| object.type == 4 || object.type == 11) {
			Identifier p = (Identifier) object;

			p.mTotalTransformationMatrix = transformationStack.peek().clone();

			this.getAllObjects().add(p);

			if (p.holdsLight) {
				float[] lightMV = new float[16];
				Matrix.multiplyMM(lightMV, 0, p.lightChild.getView(), 0,
						p.mTotalTransformationMatrix, 0);
				p.lightChild.setMVMatrix(lightMV);
			}
		}

		if (object.hasChildren) {
			Group p = (Group) object;
			for (int i = 0; i < p.children.size(); i++) {
				build(p.children.get(i));
			}
		}

		transformationStack.pop();
		return;
	}

	// ////////////////////////////////////////////////////////
	// /Searches the scene graph for an object with the same colorID as searchID
	// for picking
	// ////////////////////////////////////////////////////////

	public void searchColor(int searchID, Parent object, Identifier out) {

		if (renderer.picked)
			return;

		for (Identifier p : getAllObjects()) {
			if (searchID == p.colorID) {
				renderer.pickingID = p;
				renderer.picked = true;
				return;
			}
		}

		return;
	}

	/**
	 * @param allObjects the allObjects to set
	 */
	public void setAllObjects(LinkedList<Identifier> allObjects) {
		this.allObjects = allObjects;
	}

	/**
	 * @return the allObjects
	 */
	public LinkedList<Identifier> getAllObjects() {
		return allObjects;
	}

	/**
	 * @param allLights the allLights to set
	 */
	public void setAllLights(LinkedList<Identifier> allLights) {
		this.allLights = allLights;
	}

	/**
	 * @return the allLights
	 */
	public LinkedList<Identifier> getAllLights() {
		return allLights;
	}

}